package cn.redragon.soa.config.web;

import static cn.redragon.soa.common.core.web.ResponseDTO.CODE_NODE;
import static cn.redragon.soa.common.core.web.ResponseDTO.DATA_NODE;
import static cn.redragon.soa.common.core.web.ResponseDTO.OK;

import cn.redragon.soa.common.core.exception.ServiceException;
import cn.redragon.soa.common.core.exception.SystemError;
import cn.redragon.soa.common.core.util.JsonUtil;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.RuntimeJsonMappingException;
import feign.Client;
import feign.Logger;
import feign.Util;
import feign.codec.Decoder;
import feign.codec.ErrorDecoder;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.commons.httpclient.OkHttpClientConnectionPoolFactory;
import org.springframework.cloud.commons.httpclient.OkHttpClientFactory;
import org.springframework.cloud.openfeign.support.FeignHttpClientProperties;
import org.springframework.context.annotation.Bean;

@Slf4j
public abstract class CommonFeignConfig {

    @Bean
    @ConditionalOnMissingBean(Logger.class)
    public Logger feignLogger() {
        return new CommonFeignLogger();
    }

    @Bean
    @ConditionalOnMissingBean(Logger.Level.class)
    public Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }

    @Bean
    @ConditionalOnMissingBean(OkHttpClient.class)
    public Client feignClient(OkHttpClient client) {
        return new feign.okhttp.OkHttpClient(client);
    }

    @Bean
    @ConditionalOnMissingBean(ConnectionPool.class)
    public ConnectionPool connectionPool(FeignHttpClientProperties httpClientProperties, OkHttpClientConnectionPoolFactory connectionPoolFactory) {
        int maxTotalConnections = httpClientProperties.getMaxConnections();
        long timeToLive = httpClientProperties.getTimeToLive();
        TimeUnit ttlUnit = httpClientProperties.getTimeToLiveUnit();
        return connectionPoolFactory.create(maxTotalConnections, timeToLive, ttlUnit);
    }

    @Bean
    @ConditionalOnMissingBean(OkHttpClient.class)
    public OkHttpClient client(OkHttpClientFactory httpClientFactory, ConnectionPool connectionPool, FeignHttpClientProperties httpClientProperties) {
        boolean followRedirects = httpClientProperties.isFollowRedirects();
        int connectTimeout = httpClientProperties.getConnectionTimeout();
        boolean disableSslValidation = httpClientProperties.isDisableSslValidation();
        return httpClientFactory.createBuilder(disableSslValidation).connectTimeout(connectTimeout, TimeUnit.MILLISECONDS).followRedirects(followRedirects)
            .connectionPool(connectionPool).build();
    }

    @Bean
    public Decoder responseDecode() {
        return (response, type) -> {
            if (response.body() == null) {
                return null;
            }
            Reader reader = response.body().asReader(Util.UTF_8);
            if (!reader.markSupported()) {
                reader = new BufferedReader(reader, 1);
            }
            try {
                // Read the first byte to see if we have any data
                reader.mark(1);
                if (reader.read() == -1) {
                    return null; // Eagerly returning null avoids "No content to map due to end-of-input"
                }
                reader.reset();

                JsonNode node = JsonUtil.getObjectMapper().readTree(reader);
                String code = node.has(CODE_NODE) ? node.get(CODE_NODE).asText() : StringUtils.EMPTY;
                JsonNode dataNode = node.get(DATA_NODE);

                if (!OK.equals(code)) {

                    log.error("Call Internal server error ! response body [{}] . Request URL: [{}]", node,
                              response.request().url());
                    throw new ServiceException(code, dataNode);
                }

                if (dataNode == null || dataNode.isNull()) {
                    return null;
                }

                return JsonUtil.getObjectMapper().readValue(dataNode.toString(), JsonUtil.getObjectMapper().constructType(type));
            } catch (RuntimeJsonMappingException e) {
                if (e.getCause() instanceof IOException) {
                    throw (IOException) e.getCause();
                }
                throw e;
            }

        };
    }

    @Bean
    public ErrorDecoder errorDecoder() {
        return (methodKey, response) -> {
            try {
                log.error("Call Internal server error ! methodKey [{}} , response status [{}], response body [{}]",
                          methodKey,
                          response.status(),
                          response.body() != null ?
                              IOUtils.toString(response.body().asInputStream(), StandardCharsets.UTF_8.displayName()) : null);
            } catch (IOException e) {
                log.error("Cannot parse response body.");

                log.error(
                    "Call Internal server error with methodKey [{}} , response status [{}], response body [unknown]",
                    methodKey,
                    response.status());
            }

            return new ServiceException(SystemError.REMOTE_REST_ERROR, "Internal Service");
        };
    }
}
